DLO-JZ Data Augmentation - Jour 4¶

car

Objet du notebook¶

Dans ce TP, on se focalise sur les **problématiques de performance liées à la *Data Augmentation***.

Les opérations de transformation des données d'entrée se font en général sur le CPU. On verra dans ce TP qu'il est possible de les déléguer au GPU si les ressources de calcul offertes par le CPU ne sont pas suffisantes.

Ce TP est divisé en trois parties, correspondant à trois types d'augmentation de données à implémenter :

  • TP 1 : RandAugment sur CPU
  • TP 2 : mixup sur CPU et GPU
  • TP 3 : CutMix sur GPU

Les cellules dans ce notebook ne sont pas prévues pour être modifiées, sauf rares exceptions indiquées dans les commentaires. Les TP se feront en modifiant les codes dlojz_da_X.py.

Les directives de modification seront marquées par l'étiquette TODO dans le notebook suivant.

Les solutions sont présentes dans le répertoire solutions/.

Notebook rédigé par l'équipe assistance IA de l'IDRIS, février 2024


Environnement de calcul¶

Les fonctions python de gestion de queue SLURM dévelopées par l'IDRIS et les fonctions dédiées à la formation DLO-JZ sont à importer.

Le module d'environnement pour les jobs et la taille des images sont fixés pour ce notebook.

TODO : choisir un pseudonyme (maximum 5 caractères) pour vous différencier dans la queue SLURM et dans les outils collaboratifs pendant la formation et la compétition.

In [2]:
from idr_pytools import display_slurm_queue, gpu_jobs_submitter, search_log
from dlojz_tools import controle_technique, compare, GPU_underthehood, plot_accuracy, lrfind_plot, imagenet_starter, turbo_profiler
MODULE = 'pytorch-gpu/py3/2.3.0'
account = 'for@a100'
name = 'pseudo'   ## Pseudonyme à choisir

Gestion de la queue SLURM¶

Pour afficher vos jobs dans la queue SLURM :

In [3]:
display_slurm_queue(name)
 Done!

Remarque: Cette fonction est utilisée plusieurs fois dans ce notebook. Elle permet d'afficher la queue de manière dynamique, rafraichie toutes les 5 secondes. Elle ne s'arrête que lorsque la queue est vide. Si vous désirez reprendre la main sur le notebook, il vous suffira d'arrêter manuellement la cellule avec le bouton stop. Cela a bien sûr aucun impact les jobs soumis.

Si vous voulez retirer TOUS vos jobs de la queue SLURM, décommenter et exécuter la cellule suivante :

In [4]:
#!scancel -u $USER

Si vous voulez retirer UN de vos jobs de la queue SLURM, décommenter, compléter et exécuter la cellule suivante :

In [5]:
#!scancel <jobid>

Différence entre deux scripts¶

Pour comparer son code avec les solutions mises à disposition, la fonction suivante permet d'afficher une page html contenant un différentiel de fichiers texte.

In [13]:
s1 = "dlojz_da_2.py"
s2 = "./solutions/dlojz_da_2.py"
#s1 = "mixup.py"
#s2 = "solutions/mixup-solution.py"
compare(s1, s2)

Voir le résultat du différentiel de fichiers sur la page suivante (attention au spoil !) :

compare.html


Garage - Mise à niveau¶

On fixe le batch size et la taille d'image pour ce TP.

In [14]:
bs_optim = 512
image_size = 224

TP_DA_1 : RandAugment¶

Le but de ce TP est d'ajouter la transformation RandAugment (disponible dans torchvision) dans la liste des transformations pour la Data Augmentation et de mesurer son impact sur la performance du code.

Voir la documentation torchvision sur RandAugment.

Vous pouvez exécuter les cellules suivantes pour observer l'effet de la transformation RandAugment :

In [8]:
import os
import torchvision
import torchvision.transforms as transforms
import torchvision.models as models
import torch
import numpy as np
import matplotlib.pyplot as plt

transform = transforms.Compose([ 
        transforms.RandomResizedCrop(image_size),  # Random resize - Data Augmentation
        transforms.RandomHorizontalFlip(),  # Horizontal Flip - Data Augmentation
        transforms.RandAugment(5, 9),       # Random Augmentation 5: n operations, 9 : magnitude 
        transforms.ToTensor()               # convert the PIL Image to a tensor
        ])
    
    
train_dataset = torchvision.datasets.ImageNet(root=os.environ['ALL_CCFRSCRATCH']+'/imagenet',
                                                  transform=transform)
train_dataset
Out[8]:
Dataset ImageNet
    Number of datapoints: 1281167
    Root location: /lustre/fsn1/projects/idris/for/commun/imagenet
    Split: train
    StandardTransform
Transform: Compose(
               RandomResizedCrop(size=(224, 224), scale=(0.08, 1.0), ratio=(0.75, 1.3333), interpolation=bilinear, antialias=True)
               RandomHorizontalFlip(p=0.5)
               RandAugment(num_ops=5, magnitude=9, num_magnitude_bins=31, interpolation=InterpolationMode.NEAREST, fill=None)
               ToTensor()
           )
In [9]:
%%time

train_loader = torch.utils.data.DataLoader(dataset=train_dataset,    
                                           batch_size=4,
                                           shuffle=True)
batch = next(iter(train_loader))
print('X train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[0].shape, batch[0].dtype, batch[0].element_size()*batch[0].nelement()))
print('Y train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[1].shape, batch[1].dtype, batch[1].element_size()*batch[1].nelement()))

for i in range(4):
    img = batch[0][i].numpy().transpose((1,2,0))
    plt.imshow(img)
    plt.axis('off')
    plt.show()
X train batch, shape: torch.Size([4, 3, 224, 224]), data type: torch.float32, Memory usage: 2408448 bytes
Y train batch, shape: torch.Size([4]), data type: torch.int64, Memory usage: 32 bytes
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
CPU times: user 1.44 s, sys: 270 ms, total: 1.71 s
Wall time: 1.77 s

Transformation RandAugment sur CPU¶

TODO : dans le script dlojz_da_1.py :

  • Rajouter la transformation RandAugment dans la liste des transformations des images pour le training avec le paramétrage suivant : Nombre d'opérations = 5, Magnitude = 9.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [15]:
command = f'dlojz_da_1.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 8 cpus per task
Submitted batch job 250344
jobid = ['250344']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [16]:
#jobid = ['887754']
In [17]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            250344    gpu_p5   pseudo  cfor032 CG       1:13      1 jean-zay-iam19

 Done!
In [18]:
controle_technique(jobid)
Train throughput: 1160.67 images/second
GPU throughput: 1161.63 images/second
epoch time: 1104.13 seconds
-----------
training step time average (fwd/bkwd on GPU): 0.440759 sec (7.3%/92.8%) +/- 0.003825
loading step time average (IO + CPU to GPU transfer): 0.000364 sec +/- 0.000332

Click here to display the log file

In [19]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 41.978715 s
No description has been provided for this image

Commentaires

TP_DA_2 : mixup¶

Le but de ce TP est de :

  • appliquer la transformation mixup et mesurer son impact sur la performance du code ;
  • porter la transformation sur GPU.

La transformation mixup n'est pas disponible dans torchvision, la fonction est disponible dans le script mixup.py. On notera que cette transformation impacte à la fois l'image et le label.

On choisira, comme cela est fait habituellement, de mixer 2 images présentes dans le batch généré par le DataLoader. Donc cette transformation sera faite dans la boucle d'apprentissage après génération du batch et après toute autre transformation liée à la Data Augmentation.

Vous pouvez exécuter les cellules suivantes pour observer l'effet de la transformation mixup :

In [20]:
import os
import torchvision
import torchvision.transforms as transforms
import torchvision.models as models
import torch
import numpy as np
import matplotlib.pyplot as plt

transform = transforms.Compose([ 
        transforms.RandomResizedCrop(image_size),  # Random resize - Data Augmentation
        transforms.RandomHorizontalFlip(),  # Horizontal Flip - Data Augmentation
        transforms.ToTensor()               # convert the PIL Image to a tensor
        ])
    
    
train_dataset = torchvision.datasets.ImageNet(root=os.environ['ALL_CCFRSCRATCH']+'/imagenet',
                                                  transform=transform)
train_dataset
Out[20]:
Dataset ImageNet
    Number of datapoints: 1281167
    Root location: /lustre/fsn1/projects/idris/for/commun/imagenet
    Split: train
    StandardTransform
Transform: Compose(
               RandomResizedCrop(size=(224, 224), scale=(0.08, 1.0), ratio=(0.75, 1.3333), interpolation=bilinear, antialias=True)
               RandomHorizontalFlip(p=0.5)
               ToTensor()
           )
In [21]:
from mixup import mixup_data
In [22]:
%%time

train_loader = torch.utils.data.DataLoader(dataset=train_dataset,    
                                           batch_size=16,
                                           shuffle=True)
batch = next(iter(train_loader))
print('X train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[0].shape, batch[0].dtype, batch[0].element_size()*batch[0].nelement()))
print('Y train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[1].shape, batch[1].dtype, batch[1].element_size()*batch[1].nelement()))

imgs, targets = batch
imgs, targets = mixup_data(imgs, targets, num_classes=1000, alpha=2)        ## Transformation mixup

for i in range(4):
    img = imgs[i].numpy().transpose((1,2,0))
    plt.imshow(img)
    plt.axis('off')
    plt.show()
    print(f'target : {torch.max(targets, dim=1)[1][i]}, lambda : {torch.max(targets, dim=1)[0][i]}')
X train batch, shape: torch.Size([16, 3, 224, 224]), data type: torch.float32, Memory usage: 9633792 bytes
Y train batch, shape: torch.Size([16]), data type: torch.int64, Memory usage: 128 bytes
No description has been provided for this image
target : 15, lambda : 0.8903761506080627
No description has been provided for this image
target : 318, lambda : 0.6539812088012695
No description has been provided for this image
target : 192, lambda : 0.7615662217140198
No description has been provided for this image
target : 58, lambda : 0.6203753352165222
CPU times: user 3.92 s, sys: 273 ms, total: 4.2 s
Wall time: 4.41 s

Paramètre alpha pour la beta distribution

Dans le script mixup.py, la variable _lambda correspond à la proportion conservée de la première image. Elle est choisie aléatoirement suivant une distribution bêta définie sur [0, 1].

Le paramètre alpha agit sur la forme de la distribution bêta. alpha = 1 correspond à une distribution uniforme, alpha < 1 favorise un tirage au sort de valeurs proches des bornes 0. ou 1., et alpha > 1 favorise un tirage au sort de valeurs proches du centre 0.5.

In [23]:
for alpha in [0.5, 1., 2.]:
    plt.hist(np.random.beta(alpha, alpha, 1000000), bins=50, density=True, histtype='step')
    plt.title(f'alpha={alpha}')
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Transformation mixup sur CPU¶

TODO : dans le script dlojz_da_2.py :

  • Importer la transformation mixup
from mixup import mixup_data
  • Rajouter la transformation mixup dans la boucle d'apprentissage avant d'envoyer le batch d'images et de labels au GPU, avec le paramétrage : num_classes=1000, alpha=2.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [24]:
command = f'dlojz_da_2.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 8 cpus per task
Submitted batch job 250377
jobid = ['250377']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [25]:
#jobid = ['887894']
In [26]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            250377    gpu_p5   pseudo  cfor032  R       1:05      1 jean-zay-iam10

 Done!
In [27]:
controle_technique(jobid)
Train throughput: 764.53 images/second
GPU throughput: 1183.94 images/second
epoch time: 1676.23 seconds
-----------
training step time average (fwd/bkwd on GPU): 0.432455 sec (7.5%/92.5%) +/- 0.002306
loading step time average (IO + CPU to GPU transfer): 0.237234 sec +/- 0.019215

Click here to display the log file

In [28]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 49.20918 s
No description has been provided for this image

Transformation mixup sur GPU¶

TODO : dans le script dlojz_da_2.py :

  • Appliquer la transformation mixup dans la boucle d'apprentissage après avoir envoyé le batch d'images et de labels au GPU, avec le paramétrage : num_classes=1000, alpha=2, device=gpu.

TODO : dans le script mixup.py :

  • Ajouter le paramètre device=device à chaque fois que l'on crée un nouveau Tensor pour qu'il soit stocké en mémoire au bon emplacement (CPU ou GPU).

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [34]:
command = f'dlojz_da_2.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 8 cpus per task
Submitted batch job 250407
jobid = ['250407']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [35]:
#jobid = ['887909']
In [36]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            250407    gpu_p5   pseudo  cfor032  R       0:55      1 jean-zay-iam10

 Done!
In [37]:
controle_technique(jobid)
Train throughput: 1140.60 images/second
GPU throughput: 1182.01 images/second
epoch time: 1123.56 seconds
-----------
training step time average (fwd/bkwd on GPU): 0.433159 sec (7.5%/92.6%) +/- 0.002422
loading step time average (IO + CPU to GPU transfer): 0.015728 sec +/- 0.000478

Click here to display the log file

In [38]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 36.883998 s
No description has been provided for this image

Commentaires

TP_DA_3 : CutMix¶

Le but de ce TP est de :

  • appliquer la transformation CutMix et mesurer son impact sur la performance du code ;
  • adapter l'implémentation de la tranformation CutMix au calcul GPU.

La transformation CutMix n'est pas disponible dans torchvision, la fonction est disponible dans le script cutmix.py. On notera que cette transformation impacte à la fois l'image et le label.

On choisira, comme cela est fait habituellement, de mixer 2 images présentes dans le batch généré par le dataloader. Donc cette transformation sera faite dans la boucle d'apprentissage après génération du batch et donc après toute autre transformation liée à la Data Augmentation.

Dans le script cutmix.py, la variable _lambda correspond à la proportion conservée de la première image. Elle est choisie aléatoirement suivant une distribution uniforme définie sur [0, 1].

In [39]:
import os
import torchvision
import torchvision.transforms as transforms
import torchvision.models as models
import torch
import numpy as np
import matplotlib.pyplot as plt

transform = transforms.Compose([ 
        transforms.RandomResizedCrop(image_size),  # Random resize - Data Augmentation
        transforms.RandomHorizontalFlip(),  # Horizontal Flip - Data Augmentation
        transforms.ToTensor()               # convert the PIL Image to a tensor
        ])
    
    
train_dataset = torchvision.datasets.ImageNet(root=os.environ['ALL_CCFRSCRATCH']+'/imagenet',
                                                  transform=transform)
train_dataset
Out[39]:
Dataset ImageNet
    Number of datapoints: 1281167
    Root location: /lustre/fsn1/projects/idris/for/commun/imagenet
    Split: train
    StandardTransform
Transform: Compose(
               RandomResizedCrop(size=(224, 224), scale=(0.08, 1.0), ratio=(0.75, 1.3333), interpolation=bilinear, antialias=True)
               RandomHorizontalFlip(p=0.5)
               ToTensor()
           )
In [40]:
from cutmix import cutmix_data
In [41]:
%%time

train_loader = torch.utils.data.DataLoader(dataset=train_dataset,    
                                           batch_size=16,
                                           shuffle=True)
batch = next(iter(train_loader))
print('X train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[0].shape, batch[0].dtype, batch[0].element_size()*batch[0].nelement()))
print('Y train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[1].shape, batch[1].dtype, batch[1].element_size()*batch[1].nelement()))

imgs, targets = batch
imgs, targets = cutmix_data(imgs, targets, num_classes=1000)

for i in range(4):
    img = imgs[i].numpy().transpose((1,2,0))
    plt.imshow(img)
    plt.axis('off')
    plt.show()
    print(f'target : {torch.max(targets, dim=1)[1][i]}, lambda : {torch.max(targets, dim=1)[0][i]}')
X train batch, shape: torch.Size([16, 3, 224, 224]), data type: torch.float32, Memory usage: 9633792 bytes
Y train batch, shape: torch.Size([16]), data type: torch.int64, Memory usage: 128 bytes
No description has been provided for this image
target : 337, lambda : 0.56644606590271
No description has been provided for this image
target : 685, lambda : 0.8939732313156128
No description has been provided for this image
target : 462, lambda : 0.9644849896430969
No description has been provided for this image
target : 569, lambda : 0.8173828125
CPU times: user 4.42 s, sys: 254 ms, total: 4.67 s
Wall time: 4.86 s

Transformation CutMix sur GPU¶

TODO : dans le script dlojz_da_3.py :

  • Importer la transformation CutMix
from cutmix import cutmix_data
  • Rajouter la transformation CutMix dans la boucle d'apprentissage après avoir envoyé le batch d'images et de labels au GPU, avec le paramétrage : num_classes=1000, device=gpu.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [42]:
command = f'dlojz_da_3.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 8 cpus per task
Submitted batch job 250441
jobid = ['250441']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [43]:
#jobid = ['887951']
In [44]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            250441    gpu_p5   pseudo  cfor032  R       0:55      1 jean-zay-iam10

 Done!
In [45]:
controle_technique(jobid)
Train throughput: 1000.36 images/second
GPU throughput: 1183.18 images/second
epoch time: 1281.07 seconds
-----------
training step time average (fwd/bkwd on GPU): 0.432734 sec (7.2%/94.9%) +/- 0.043066
loading step time average (IO + CPU to GPU transfer): 0.079082 sec +/- 0.001052

Click here to display the log file

In [46]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 39.048925 s
No description has been provided for this image

Optimisation de la transformation CutMix¶

Le code précédent traite les images du batch une par une, de manière séquentielle (boucle for) :

mixed_x = x
for i in range(len(mixed_x)): # loop over images
            mixed_x[i,:,x1[i]:x2[i],y1[i]:y2[i]] = x[s_index[i],:,x1[i]:x2[i],y1[i]:y2[i]]

Le but de cette partie est d'optimiser le code de CutMix en générant davantage de parallélisme pour profiter du GPU. Il s'agit de supprimer la boucle for et de manipuler directement des batches de tenseurs.

Le travail va porter sur la définition de deux batches de masques mask_int et mask_ext de taille [batch_size,n_channels,weight,height] que l'on appliquera de la manière suivante :

mixed_x = mask_ext * x + mask_int * x[s_index, :]

La fonction à implémenter est la suivante :

def cut_mask(x1, x2, y1, y2, batch_size, W, H, device=None):
    
    ### TODO
    mask_ext, mask_int = None, None
    
    return mask_ext, mask_int

Arguments :

  • x1 : vecteur de longueur batch_size avec la coordonnée min dans la largeur pour chaque image du batch
  • x2 : vecteur de longueur batch_size avec la coordonnée max dans la largeur pour chaque image du batch
  • y1 : vecteur de longueur batch_size avec la coordonnée min dans la hauteur pour chaque image du batch
  • y2 : vecteur de longueur batch_size avec la coordonnée max dans la hauteur pour chaque image du batch
  • batch_size : nombre d'images dans le batch
  • W : largeur des images
  • H : hauteur des images
  • device : unité de calcul ('cpu' ou 'gpu')

Retours:

  • mask_ext : tenseur de taille [batch_size,n_channels,weight,height] contenant la valeur False ou 0 à l'intérieur de la fenêtre et True ou 1 à l'extérieur
  • mask_int : tenseur de taille [batch_size,n_channels,weight,height] contenant la valeur True ou 1 à l'intérieur de la fenêtre et False ou 0 à l'extérieur

Création d'un batch de masques

Dans un premier temps, pour comprendre la procédure, nous travaillerons avec un batch de 3 images de taille 32x32.

In [47]:
import torch
import numpy as np
import matplotlib.pyplot as plt
batch_size = 3
W = 32
H = 32

En entrée, on connait les coordonnées des coins de la fenêtre pour chaque image du batch (voir illustration ci-dessous).

In [48]:
# coordonnee min dans la largeur pour chaque image du batch
x1 = torch.Tensor([10, 5, 23]).int()
# coordonne max dans la largeur pour chaque image du batch
x2 =  torch.Tensor([20, 25, 31]).int()
# coordonnee min dans la hauteur pour chaque image du batch
y1 =  torch.Tensor([5, 10, 0]).int()
# coordonne max dans la hauteur pour chaque image du batch
y2 =  torch.Tensor([10, 22, 20]).int()
No description has been provided for this image

1. Création de w_int et h_int

Pour construire mask_int, on va d'abord créer un batch de vecteurs ligne "largeur" w_int et un batch de vecteurs colonne "hauteur" de masques h_int (voir illustration ci-dessus).

Variables utiles : batch_size, W, H, x1, x2, y1, y2.

Voir la documentation PyTorch pour la manipulation de tenseurs : documentation torch.

Résultats attendus : (voir illustration ci-dessous)

  • un batch de vecteurs ligne "largeur" w_int, tenseur de taille [3, 1, 32] contenant des True ou 1 si x1 ⩽ x ⩽ x2, False ou 0 sinon
  • un batch de vecteurs colonne "hauteur" h_int, tenseur de taille [3, 32, 1] contenant des True ou 1 si y1 ⩽ y ⩽ y2, False ou 0 sinon résultat
Pistes de solutions

Nous avons trouvé 2 solutions pour résoudre ce problème.

  • En utilisant la fonction torch.logical_and : il s'agit d'initialiser des tenseurs à [x, x=0,...,31] (respectivement [y, y=0,...,31]) et d'utiliser torch.logical_and() pour appliquer les conditions x1 ⩽ x and x ⩽ x2 (respectivement y1 ⩽ y and y ⩽ y2). Le résultat est un tenseur contenant des opérateurs logiques True et False.

  • En utilisant la fonction torch.cumsum : les tenseurs sont initialisés à 0, on donne la valeur 1 au x1ème élément (respectivement y1ème), la valeur -1 au x2ème élément (respectivement y2ème), puis on utilise la fonction torch.cumsum pour faire la somme cumulée des éléments. On obtient un tenseur contenant des 0 et des 1.

Dans tous les cas, il faudra jouer avec les dimensions des tenseurs. Les fonctions utiles sont : torch.arange(), .size(), .repeat(), .view(), .unsqueeze(), .zeros(), ...

Il existe certainement d'autres solutions.

Rappel : ne jamais utiliser de for nulle part !

In [49]:
# initialisation du tenseur w_int avec les valeurs [0,...,31]
w_int = torch.arange(W).repeat(batch_size,1,1) # batch de vecteurs ligne 
# Returns the mask applying ((x1 ⩽ x) and (x ⩽ x2))
w_int = torch.logical_and(w_int >= x1.view(-1,1,1), w_int <= x2.view(-1,1,1)) # vecteurs ligne

# assert w_int has the correct size
assert w_int.size() == torch.Size([3,1,32])
In [50]:
for wx in w_int:
    plt.imshow(wx)
    plt.colorbar()
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
In [51]:
# initialisation du tenseur h_int avec les valeurs [0,...,31]
h_int = torch.arange(H).repeat(batch_size,1).unsqueeze(2) # batch de vecteurs colonne
# Returns the mask applying ((y1 ⩽ y) and (y ⩽ y2))
h_int = torch.logical_and(h_int >= y1.view(-1,1,1), h_int <= y2.view(-1,1,1)) # vecteurs colonne

# assert h_int has the correct size
assert h_int.size() == torch.Size([3,32,1])
In [52]:
for hx in h_int:
    plt.imshow(hx)
    plt.colorbar()
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
Solution 1 avec torch.logical_and
# initialisation du tenseur w_int avec les valeurs [0,...,31]
w_int = torch.arange(W).repeat(batch_size,1,1) # batch de vecteurs ligne 
# Returns the mask applying ((x1 ⩽ x) and (x ⩽ x2))
w_int = torch.logical_and(w_int >= x1.view(-1,1,1), w_int <= x2.view(-1,1,1)) # vecteurs ligne

# initialisation du tenseur h_int avec les valeurs [0,...,31]
h_int = torch.arange(H).repeat(batch_size,1).unsqueeze(2) # batch de vecteurs colonne
# Returns the mask applying ((y1 ⩽ y) and (y ⩽ y2))
h_int = torch.logical_and(h_int >= y1.view(-1,1,1), h_int <= y2.view(-1,1,1)) # vecteurs colonne
Solution 2 avec torch.cumsum

On initialise les éléments correspondant aux coordonnées x1 et y1 à 1.
On initialise les éléments correspondant aux coordonnées x2 et y2 à -1.
Puis on utilise la fonction torch.cumsum pour remplir chaque intervalle [x1,x2] et [y1,y2] de 1, le reste de 0.

Remarque : il y a une petite erreur dans cette solution qui n'a pas d'impact majeur. +1 sur votre appréciation finale si vous la trouvez !!

w_int = torch.zeros(batch_size,1,W) # vecteurs ligne 
w_int[range(batch_size),0,x1] = 1.
w_int[range(batch_size),0,x2] = -1.
w_int = torch.cumsum(w_int, dim=2).bool() # vecteurs ligne

h_int = torch.zeros(batch_size,H,1) # vecteurs colonne
h_int[range(batch_size),y1,0] = 1.
h_int[range(batch_size),y2,0] = -1.
h_int = torch.cumsum(h_int, dim=1).bool() # vecteurs colonne

Solution +++

Avec la solution précédente, les bornes x2 et y2 sont exclues de la fenêtre. Pour les inclure, il faudrait définir la valeur -1 sur les x2+1ème et y2+1ème éléments :

w_int[range(batch_size),0,x2+1] = -1.
h_int[range(batch_size),y2+1,0] = -1.

Cela entraîne des cas particuliers si x2=31 ou y2=31. Pour gérer ces exceptions sans introduire de if :

# if x2==31, set w_int(.,0,31)=0, otherwize set w_int(.,0,x2+1)=-1
w_int[.,0,torch.minimum(torch.tensor([31]).repeat(batch_size),x2+1)]=torch.maximum(torch.tensor([-1.]).repeat(batch_size),x2-31)
# if y2==31, set h_int(.,31,0)=0, otherwize set h_int(.,y2+1,0)=-1
h_int[.,torch.minimum(torch.tensor([31]).repeat(batch_size),y2+1,0)]=torch.maximum(torch.tensor([-1.]).repeat(batch_size),y2-31)

2. Création des batches de masques intérieurs et extérieurs

  • Multiplication des vecteurs h_int et w_int pour obtenir les masques intérieurs pour chaque image du batch.
In [53]:
# multiplication des vecteurs colonne "hauteur" h_int par les vecteurs ligne "largeur" w_int
mask_int = h_int*w_int
In [54]:
# visualisation des masques intérieurs pour chaque image du batch
for m in mask_int:
    plt.imshow(m)
    plt.colorbar()
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
Solution
# multiplication des vecteurs colonne "hauteur" h_int par les vecteurs ligne "largeur" w_int
mask_int = h_int*w_int
  • Puis création des masques extérieurs à partir des masques intérieurs.
Aide

Par exemple en utilisant la fonction torch.logical_not.

In [55]:
# les masques extérieurs sont les complémentaires des masques intérieurs
mask_ext = torch.logical_not(mask_int)
In [56]:
# visualisation des masques extérieurs
for m in mask_ext:
    plt.imshow(m)
    plt.colorbar()
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
Solution
# les masques extérieurs sont les complémentaires des masques intérieurs
mask_ext = torch.logical_not(mask_int)

Implémentation de la fonction de création d'un batch de masques

Maintenant, l'idée est d'implémenter ce qui a été fait dans les cellules précédentes dans une fontion générique, en ajoutant un choix sur le device d'exécution.

TODO : implémenter la fonction de création des masques dans la cellule suivante. Les entrées de la fonction sont :

  • les coordonnées x1, x2, y1, y2,
  • le batch_size,
  • la largeurW de l'image,
  • la hauteur H de l'image,
  • le device de calcul.

Important : Pour les images RGB (channel de 3), il faut rajouter une dimension en deuxième position dans les masques finaux (doc .unsqueeze()) :

# rajouter une dimension en 2e position pour pouvoir traiter des images RGB
    mask_int = mask_int.unsqueeze(1) 
    mask_ext = mask_ext.unsqueeze(1)

Attention : Ne pas oublier le paramètre device=device à chaque création d'un nouveau Tensor. Par exemple pour :

w_int = torch.zeros(batch_size,1,W,device=device)
In [57]:
def cut_mask(x1, x2, y1, y2, batch_size, W, H, device=None):
    
    #TODO
    # initialisation du tenseur w_int avec les valeurs [0,...,31]
    w_int = torch.arange(W, device=device).repeat(batch_size,1,1) # batch de vecteurs ligne
    # Returns the mask applying ((x1 ⩽ x) and (x ⩽ x2))
    w_int = torch.logical_and(w_int >= x1.view(-1,1,1), w_int <= x2.view(-1,1,1)) # vecteurs ligne 
    
    # initialisation du tenseur h_int avec les valeurs [0,...,31]
    h_int = torch.arange(H, device=device).repeat(batch_size,1).unsqueeze(2) # batch de vecteurs colonne
    # Returns the mask applying ((y1 ⩽ y) and (y ⩽ y2))
    h_int = torch.logical_and(h_int >= y1.view(-1,1,1), h_int <= y2.view(-1,1,1)) # vecteurs colonne
    
    # multiplication des vecteurs colonne "hauteur" h_int par les vecteurs ligne "largeur" w_int
    mask_int = h_int*w_int

    # les masques extérieurs sont les complémentaires des masques intérieurs
    mask_ext = torch.logical_not(mask_int)
    
    # rajouter une dimension en 2e position pour pouvoir traiter des images RGB
    mask_int = mask_int.unsqueeze(1) 
    mask_ext = mask_ext.unsqueeze(1)
    
    return mask_ext, mask_int

Test de la fonction implémentée¶

In [66]:
%%time

train_loader = torch.utils.data.DataLoader(dataset=train_dataset,    
                                           batch_size=16,
                                           shuffle=True)
batch = next(iter(train_loader))
print('X train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[0].shape, batch[0].dtype, batch[0].element_size()*batch[0].nelement()))
print('Y train batch, shape: {}, data type: {}, Memory usage: {} bytes'
      .format(batch[1].shape, batch[1].dtype, batch[1].element_size()*batch[1].nelement()))

imgs, targets = batch
X train batch, shape: torch.Size([16, 3, 224, 224]), data type: torch.float32, Memory usage: 9633792 bytes
Y train batch, shape: torch.Size([16]), data type: torch.int64, Memory usage: 128 bytes
CPU times: user 1.87 s, sys: 16.9 ms, total: 1.88 s
Wall time: 1.83 s
In [67]:
batch_size = 16
W = image_size
H = image_size
In [68]:
lam = torch.rand(batch_size)
s_index = torch.randperm(batch_size)      # Shuffle index
rand_x = torch.randint(W, (batch_size,))
rand_y = torch.randint(H, (batch_size,))
cut_rat = torch.sqrt(1. - lam) ## cut ratio according to the random lambda

x1 = torch.clip(rand_x - rand_x / 2, min=0).long()
x2 = torch.clip(rand_x + rand_x / 2, max=W-1).long()
y1 = torch.clip(rand_y - rand_y / 2, min=0).long()
y2 = torch.clip(rand_y + rand_y / 2, max=H-1).long()

mask_ext, mask_int = cut_mask(x1, x2, y1, y2, batch_size, W, H)
In [69]:
# vérifier si le masque intérieur et l'image ont le même nombre de dimensions
try:
    assert imgs.dim() == mask_int.dim()
    print('OK!')
except:
    print(f'Mismatch: \n dim imgs = {imgs.dim()} \n dim mask int = {mask_int.dim()} ')
OK!
In [70]:
# vérifier si le masque extérieur et l'image ont le même nombre de dimensions
try:
    assert imgs.dim() == mask_ext.dim()
    print('OK!')
except:
    print(f'Mismatch: \n dim imgs = {imgs.dim()} \n dim mask ext = {mask_ext.dim()} ')
OK!
In [71]:
imgs = mask_ext * imgs + mask_int * imgs[s_index, :]
In [72]:
for i in range(4):
    img = imgs[i].numpy().transpose((1,2,0))
    plt.imshow(img)
    plt.axis('off')
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Puis si le résultat est satisfaisant, intégrer la fonction dans le code cutmix.py.

Intégration de la nouvelle version dans cutmix.py¶

TODO : dans le script cutmix.py, ajouter la fonction cut_mask définie dans la cellule plus haut.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [73]:
command = f'dlojz_da_3.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 8 cpus per task
Submitted batch job 250501
jobid = ['250501']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [74]:
#jobid = ['888412']
In [75]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            250501    gpu_p5   pseudo  cfor032  R       0:55      1 jean-zay-iam10

 Done!
In [76]:
controle_technique(jobid)
Train throughput: 1139.03 images/second
GPU throughput: 1181.89 images/second
epoch time: 1125.11 seconds
-----------
training step time average (fwd/bkwd on GPU): 0.433205 sec (7.4%/92.7%) +/- 0.002677
loading step time average (IO + CPU to GPU transfer): 0.016299 sec +/- 0.000641

Click here to display the log file

In [77]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 37.17523 s
No description has been provided for this image

Garage

In [ ]: